3.4 流控制

Go精简(合并)了流控制语句,虽然某些时候不够便捷,但够用。

if…else…

条件表达式值必须是布尔类型,可省略括号,且左花括号不能另起一行。

func main() { 
   x:=3
  
   if x>5{ 
       println("a") 
    }else if x<5&&x>0{ 
       println("b") 
    }else{ 
       println("z") 
    } 
}

比较特别的是对初始化语句的支持,可定义块局部变量或执行初始化函数。

func main() { 
   x:=10
  
   if xinit();x==0{      // 优先执行xinit函数 
       println("a") 
    } 
  
   if a,b:=x+1,x+10;a<b{  // 定义一个或多个局部变量(也可以是函数返回值) 
       println(a) 
    }else{ 
       println(b) 
    } 
}

局部变量的有效范围包含整个if/else块。

对于编程初学者,可能会因条件匹配顺序不当而写出死代码(dead code)。

func main() { 
   x:=8
  
   if x>5{              // 优先判断,条件表达式结果为true
       println("a") 
    }else if x>7{        //dead code
       println("b") 
    } 
}

输出:

a

死代码是指永远不会被执行的代码,可使用专门的工具,或用代码覆盖率(code coverage)测试进行检查。某些比较智能的编译器也可主动清除死代码(dead code elimination,DCE)。

尽可能减少代码块嵌套,让正常逻辑处于相同层次。

import( 
    "errors" 
    "log" 
) 
  
func check(x int)error{ 
   if x<=0{ 
       return errors.New("x<=0") 
    } 
  
   return nil
} 
  
func main() { 
   x:=10
  
   if err:=check(x);err==nil{ 
       x++ 
       println(x) 
    }else{ 
       log.Fatalln(err) 
    } 
}

该示例中,if块显然承担了两种逻辑:错误处理和后续正常操作。基于重构原则,我们应该保持代码块功能的单一性。

func main() { 
   x:=10
  
   if err:=check(x);err!=nil{ 
       log.Fatalln(err) 
    } 
  
   x++ 
   println(x) 
}

如此,if块仅完成条件检查和错误处理,相关正常逻辑保持在同一层次。当有人试图通过阅读这段代码来获知逻辑流程时,完全可忽略if块细节。同时,单一功能可提升代码可维护性,更利于拆分重构。

当然,如须在多个条件块中使用局部变量,那么只能保留原层次,或直接使用外部变量。

import( 
    "log" 
    "strconv" 
) 
  
func main() { 
   s:= "9" 
  
   n,err:=strconv.ParseInt(s,10,64)       // 使用外部变量 
  
   if err!=nil{ 
       log.Fatalln(err) 
    }else if n<0||n>10{        // 也可考虑拆分成另一个独立if块 
       log.Fatalln("invalid number") 
    } 
  
   println(n)              // 避免if局部变量将该逻辑放到else块 
}

对于某些过于复杂的组合条件,建议将其重构为函数。

import( 
    "log" 
    "strconv" 
) 
  
func main() { 
   s:= "9" 
  
   if n,err:=strconv.ParseInt(s,10,64);err!=nil||n<0||n>10||n%2!=0{ 
       log.Fatalln("invalid number") 
    } 
  
   println("ok") 
}

函数调用虽然有一些性能损失,可却让主流程变得更加清爽。况且,条件语句独立之后,更易于测试,同样会改善代码可维护性。

import( 
    "errors" 
    "log" 
    "strconv" 
) 
  
func check(s string)error{ 
   n,err:=strconv.ParseInt(s,10,64) 
   if err!=nil||n<0||n>10||n%2!=0{ 
       return errors.New("invalid number") 
    } 
  
   return nil
} 
  
func main() { 
   s:= "9" 
  
   if err:=check(s);err!=nil{ 
       log.Fatalln(err) 
    } 
  
   println("ok") 
}

将流程和局部细节分离是很常见的做法,不同的变化因素被分隔在各自独立单元(函数或模块)内,可避免修改时造成关联错误,减少患“肥胖症”的函数数量。当然,代码单元测试也是主要原因之一。另一方面,该示例中的函数check仅被if块调用,也可将其作为局部函数,以避免扩大作用域,只是对测试的友好度会差一些。

当前编译器只能说够用,须优化的地方太多,其中内联处理做得也差强人意,所以代码维护性和性能平衡需要投入更多心力。

语言方面,最遗憾的是没有条件运算符“a>b?a:b”。有没有lambda无所谓,但没有这个却少了份优雅。加上一大堆err!=nil判断语句,对于有完美主义倾向的代码洁癖患者来说是种折磨。

switch

与if类似,switch语句也用于选择执行,但具体使用场景会有所不同。

func main() { 
   a,b,c,x:=1,2,3,2
  
   switch x{              // 将x与case条件匹配 
   case a,b:              // 多个匹配条件命中其一即可(OR),变量 
       println("a|b") 
   case c:             // 单个匹配条件 
       println("c") 
   case 4:             // 常量 
       println("d") 
   default:                
       println("z") 
    } 
}

输出:

a|b

条件表达式支持非常量值,这要比C更加灵活。相比if表达式,switch值列表要更加简洁。

编译器对if、switch生成的机器指令可能完全相同,所谓谁性能更好须看具体情况,不能作为主观判断条件。

switch同样支持初始化语句,按从上到下、从左到右顺序匹配case执行。只有全部匹配失败时,才会执行default块。

func main() { 
   switch x:=5;x{ 
   default:            // 编译器确保不会先执行default块 
       x+=100
       println(x) 
   case 5: 
       x+=50
       println(x) 
    } 
}
 

输出:

55

考虑到default作用类似else,建议将其放置在switch末尾。

相邻的空case不构成多条件匹配。

switch x{ 
case a:           // 单条件,内容为空。隐式 “case a:break;” 
case b: 
println("b") 
}

不能出现重复的case常量值。

func main() { 
   switch x:=5;x{ 
   case 5: 
       println("a") 
   case 6,5:          // 错误:duplicate case 5 in switch
       println("b") 
    } 
}

无须显式执行break语句,case执行完毕后自动中断。如须贯通后续case(源码顺序),须执行fallthrough,但不再匹配后续条件表达式。

func main() { 
   switch x:=5;x{ 
   default: 
       println(x) 
   case 5: 
       x+=10
       println(x) 
  
       fallthrough      // 继续执行下一case,但不再匹配条件表达式 
   case 6:         
       x+=20      
       println(x) 
  
        //fallthrough     // 如果在此继续fallthrough,不会执行default,完全按源码顺序 
    }              // 导致 "cannot fallthrough final case in switch" 错误 
}

输出:

15
35

注意,fallthrough必须放在case块结尾,可使用break语句阻止。

func main() { 
   switch x:=5;x{ 
   case 5: 
       x+=10
       println(x) 
  
       if x>=15{ 
           break     // 终止,不再执行后续语句 
        } 
  
       fallthrough      // 必须是case块的最后一条语句 
   case 6: 
       x+=20
       println(x) 
    } 
}

输出:


15

某些时候,switch还被用来替换if语句。被省略的switch条件表达式默认值为true,继而与case比较表达式结果匹配。

func main() { 
   switch x:=5; {     // 相当于 “switch x:=5;true{ ... }” 
   case x>5: 
       println("a") 
   case x>0&&x<=5: // 不能写成 “case x>0,x<=5”,因为多条件是OR关系 
       println("b") 
   default: 
       println("z") 
    } 
}

输出:

b

switch语句也可用于接口类型匹配,详见后续章节。

for

仅有for一种循环语句,但常用方式都能支持。

for i:=0;i<3;i++ { // 初始化表达式支持函数调用或定义局部变量 
}
 
for x<10{           // 类似 "while x<10{}" 或 "for;x<10; {}" 
   x++ 
}
 
for{             // 类似 "while true{}" 或 "for true{}" 
   break
}

初始化语句仅被执行一次。条件表达式中如有函数调用,须确认是否会重复执行。可能会被编译器优化掉,也可能是动态结果须每次执行确认。

func count()int{ 
   print("count.") 
   return 3
} 
  
func main() { 
   for i,c:=0,count();i<c;i++ {    // 初始化语句的count函数仅执行一次 
       println("a",i) 
    } 
  
   c:=0
   for c<count() {        // 条件表达式中的count重复执行 
       println("b",c) 
       c++ 
    } 
}

输出:

count.a 0
a 1
a 2
  
count.b 0
count.b 1
count.b 2

规避方式就是在初始化表达式中定义局部变量保存count结果。

可用for…range完成数据迭代,支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据。

data type       1st value       2nd value
-----------------+-----------------+------------------+------------------ 
string          index           s[index]          unicode,rune
array/slice     index           v[index] 
map             key             value
channel         element
 
func main() { 
   data:= [3]string{"a", "b", "c"} 
  
   for i,s:=range data{ 
       println(i,s) 
    } 
}

输出:

0 a
1 b
2 c

没有相关接口实现自定义类型迭代,除非基础类型是上述类型之一。

允许返回单值,或用“_”忽略

func main() { 
   data:= [3]string{"a", "b", "c"} 
  
   for i:=range data{ // 只返回1st value
       println(i,data[i]) 
    } 
  
   for_,s:=range data{  // 忽略1st value
       println(s) 
    } 
  
   for range data{     // 仅迭代,不返回。可用来执行清空channel等操作 
    } 
}

无论普通for循环,还是range迭代,其定义的局部变量都会重复使用

func main() { 
   data:= [3]string{"a", "b", "c"} 
  
   for i,s:=range data{ 
       println(&i, &s) 
    } 
}

输出:

0xc82003fe98 0xc82003fec8
0xc82003fe98 0xc82003fec8
0xc82003fe98 0xc82003fec8

这对闭包存在一些影响,相关详情,请阅读后续章节。

注意,range会复制目标数据。受直接影响的是数组,可改用数组指针或切片类型。

func main() { 
   data:= [3]int{10,20,30} 
  
   for i,x:=range data{      // 从data复制品中取值 
       if i==0{ 
           data[0] +=100
           data[1] +=200
           data[2] +=300
        } 
  
       fmt.Printf("x: %d,data: %d\n",x,data[i]) 
    } 
  
   for i,x:=range data[:] {        // 仅复制slice,不包括底层array
       if i==0{ 
           data[0] +=100
           data[1] +=200
           data[2] +=300
        } 
  
       fmt.Printf("x: %d,data: %d\n",x,data[i]) 
    } 
}

输出:

x:10,data:110
x:20,data:220           //range返回的依旧是复制值 
x:30,data:330
  
x:110,data:210          // 当i==0修改data时,x已经取值,所以是110
x:420,data:420          // 复制的仅是slice自身,底层array依旧是原对象 
x:630,data:630

相关数据类型中,字符串、切片基本结构是个很小的结构体,而字典、通道本身是指针封装,复制成本都很小,无须专门优化。

如果range目标表达式是函数调用,也仅被执行一次。

func data() []int{ 
   println("origin data.") 
   return[]int{10,20,30} 
} 
  
func main() { 
   for i,x:=range data() { 
       println(i,x) 
    } 
}

输出:

origin data. 
0 10
1 20
2 30

建议嵌套循环不要超过2层,否则会难以维护。必要时可剥离,重构为函数。

goto,continue,break

对于goto的讨伐由来已久,仿佛它是“笨蛋”标签一般。可事实上,能在很多场合见到它的身影,就连Go源码里都有很多。

$cd go/src
$grep-r-n"goto" *

单就Go 1.6的源码统计结果,goto语句就超出1000条有余。很惊讶,不是吗?虽然某些设计模式可用来消除goto语句,但在性能优先的场合,它能发挥积极作用。

使用goto前,须先定义标签。标签区分大小写,且未使用的标签会引发编译错误。

func main() { 
start:                     // 错误:label start defined and not used
   for i:=0;i<3;i++ { 
       println(i) 
  
       if i>1{ 
           goto exit
        } 
    } 
  
exit: 
   println("exit.") 
}
 

不能跳转到其他函数,或内层代码块内。

func test() { 
test: 
   println("test") 
   println("test exit.") 
} 
  
func main() { 
   for i:=0;i<3;i++ { 
   loop: 
       println(i) 
    } 
  
   goto test          // 错误:label test not defined
   goto loop          // 错误:goto loop jumps into block
}

和goto定点跳转不同,break、continue用于中断代码块执行。

  • break:用于switch、for、select语句,终止整个语句块执行。
  • continue:仅用于for循环,终止后续逻辑,立即进入下一轮循环。
func main() { 
   for i:=0;i<10;i++ { 
       if i%2==0{ 
           continue       // 立即进入下一轮循环 
        } 
  
       if i>5{ 
           break     // 立即终止整个for循环 
        } 
  
       println(i) 
    } 
}

输出:

1
3
5

配合标签,break和continue可在多层嵌套中指定目标层级。

func main() { 
outer: 
   for x:=0;x<5;x++ { 
       for y:=0;y<10;y++ { 
           if y>2{ 
               println() 
               continue outer
            } 
  
           if x>2{ 
               break outer
            } 
  
           print(x, ":",y, " ") 
        } 
    } 
}

输出:

0:0 0:1 0:2
1:0 1:1 1:2
2:0 2:1 2:2